package com.google.code.jbauble.mvc.render;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.context.Context;
import org.apache.velocity.exception.MethodInvocationException;
import org.apache.velocity.exception.ParseErrorException;
import org.apache.velocity.exception.ResourceNotFoundException;
import org.apache.velocity.io.VelocityWriter;
import org.apache.velocity.tools.ToolManager;
import org.apache.velocity.util.SimplePool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.code.jbauble.mvc.DefaultConfiguration;
import com.google.code.jbauble.mvc.Result;
import com.google.code.jbauble.mvc.ViewRender;
import com.google.code.jbauble.mvc.support.UrlHelper;
import com.google.code.jbauble.mvc.support.ViewResourceCache;
import com.google.code.jbauble.mvc.support.ViewResourceContext;
import com.opensymphony.module.sitemesh.HTMLPage;
import com.opensymphony.module.sitemesh.Page;
import com.opensymphony.module.sitemesh.PageParser;
import com.opensymphony.module.sitemesh.parser.HTMLPageParser;
import com.opensymphony.sitemesh.compatability.HTMLPage2Content;

public class VelocityViewRender implements ViewRender {
	protected final Logger log = LoggerFactory.getLogger(this.getClass());
	private DefaultConfiguration config = null;
	private boolean viewRequest = false;
	private VelocityEngine ve = null;
	private Context context = null;
	private String encoding = "utf-8";
	private ViewResourceCache cache = null;
	private String viewPath = null;
	private long cacheOvertime = 0;
	
	private static SimplePool writerPool = new SimplePool(40);
	
	public VelocityViewRender(String path, String viewPath, DefaultConfiguration config) {
		this.config = config;
		ve = new VelocityEngine();
		viewRequest = config.getProperty(DefaultConfiguration.MVC_VIEW_REQUEST).equals("true");
		encoding = config.getProperty(DefaultConfiguration.SITE_ENCODING);
		cache = ViewResourceCache.init(viewPath, getConfigOvertime(config), encoding);
		this.viewPath = viewPath;
		
		try {
			ToolManager manager = new ToolManager(false, false);
			manager.configure(path + "/WEB-INF/toolbox.xml");
			context = manager.createContext();
			
			//ve.addProperty(Velocity.RESOURCE_LOADER, "file");
			ve.addProperty("memory.resource.loader.class", "com.google.code.jbauble.mvc.support.MemoryResourceLoader");
			ve.addProperty(Velocity.RESOURCE_LOADER, "memory");
			ve.addProperty(Velocity.FILE_RESOURCE_LOADER_PATH, viewPath);
			//ve.addProperty(Velocity.FILE_RESOURCE_LOADER_CACHE, true);
			
			ve.init();
		} catch (Exception e) {
			log.error("VelocityViewRender init error.", e);
		}
	}

	private long getConfigOvertime(DefaultConfiguration conf) {
		if (conf.getProperty(DefaultConfiguration.SITE_ENV).equals("prod"))
			return Long.valueOf(conf.getProperty(DefaultConfiguration.SITE_ENV));
		return 0;
	}

	@Override
	public void render(HttpServletRequest request,
			HttpServletResponse response, Result result, Map exts) throws Exception {
		response.setContentType("text/html");
		VelocityWriter vw = null;

		try
		{
			loadViewToCache(isLayout(exts) ? exts.get("layout").toString() : null, result.getUrl());
			
			ServletOutputStream output = response.getOutputStream();
			VelocityContext vc = new VelocityContext(context);
			setVelocityContext(vc, request, result);

			Template template = ve.getTemplate(result.getUrl(), encoding);
			
			vw = (VelocityWriter)writerPool.get();
			if (vw == null)
            {
                vw = new VelocityWriter(
                		new OutputStreamWriter(output, encoding), 
                		4 * 1024, true);
            }
            else
            {
                vw.recycle(new OutputStreamWriter(output, encoding));
            }
			
			template.merge(vc, vw);
		}
		catch(ResourceNotFoundException rnfe) {
			log.error("Velocity render error." + request.getRequestURI(), rnfe);
			throw rnfe;
		}
		catch(ParseErrorException pee) {
			log.error("Velocity render error." + request.getRequestURI(), pee);
			throw pee;
		}
		catch(MethodInvocationException mie) {
			log.error("Velocity render error." + request.getRequestURI(), mie);
			throw mie;
		}
		catch(Exception e) {
			log.error("Velocity render error." + request.getRequestURI(), e);
			throw e;
		}
		finally {
            if (vw != null) {
                try {
                    vw.flush();
                }
                catch (IOException e) {
                	log.error("Velocity render error." + request.getRequestURI(), e);
                }

                vw.recycle(null);
                writerPool.put(vw);
            }
        }
	}

	private void loadViewToCache(String layout, String name) 
			throws IOException {
		if (cache.containsView(name))
			return;
		
		if (layout != null) {
			mergeView(layout, name);
		}
		else {
			cache.loadViewFromFile(name);
		}
		
	}

	private void mergeView(String layout, String name) throws IOException {
		ViewResourceContext lr = cache.loadView(layout);
		ViewResourceContext vr = cache.loadViewResourceContext(name);
		
		PageParser pageParser = new HTMLPageParser();
		Page page = pageParser.parse(vr.getViewContent().toCharArray());
		HTMLPage htmlPage = (HTMLPage)page;
		ViewResourceContext r = replaceResourceContext(lr, htmlPage, name);
		
		cache.setView(name, r);
	}

	private final String TITLE = "$title";
	private final String HEADER = "$head";
	private final String BODY = "$body";
	
	private ViewResourceContext replaceResourceContext(ViewResourceContext lr,
			HTMLPage content, String name) throws IOException {
		String html = lr.getViewContent();
		String title = content.getTitle();
		if (title == null) {
			title = "";
		}
		String header = getHtmlHeader(content);
		if (header == null) {
			header = "";
		}
		String body = getHtmlBody(content);
		if (body == null) {
			body = "";
		}
		
		int offset = 0;
		StringBuilder sb = new StringBuilder();
		int index = html.indexOf(TITLE, offset);
		if (index > -1) {
			sb.append(html.substring(offset, index));
			sb.append(title);
			offset = index + TITLE.length();
		}
		index = html.indexOf(HEADER, offset);
		if (index > -1) {
			sb.append(html.substring(offset, index));
			sb.append(header);
			offset = index + HEADER.length();
		}
		index = html.indexOf(BODY, offset);
		if (index > -1) {
			sb.append(html.substring(offset, index));
			sb.append(body);
			offset = index + BODY.length();
		}
		
		sb.append(html.substring(offset, html.length()));
		
		ViewResourceContext r = new ViewResourceContext();
		r.setOvertime(lr.getOvertime());
		r.setResourceName(name);
		r.setViewContent(sb.toString());
		return r;
	}

	private String getHtmlBody(HTMLPage content) throws IOException {
		StringWriter w = new StringWriter();
		try {
			content.writeBody(w);
			return w.toString();
		}
		finally {
			if (w != null) {
				w.close();
			}
		}
	}

	private String getHtmlHeader(HTMLPage content) throws IOException {
		StringWriter w = new StringWriter();
		try {
			content.writeHead(w);
			return w.toString();
		}
		finally {
			if (w != null) {
				w.close();
			}
		}
	}

	private boolean isLayout(Map exts) {
		return exts != null 
				&& (Boolean.valueOf(exts.get("isLayout").toString())).equals(Boolean.TRUE);
	}

	private void setVelocityContext(VelocityContext context,
			HttpServletRequest request, Result result) {
		if (viewRequest) {
			context.put("request", getRequestMap(request));
		}
		//context.put("session", getSessionMap(request.getSession()));
		Object obj = result.getParameters();
		if (obj == null)
			return;
		
		if (!Map.class.isAssignableFrom(obj.getClass())) {
			context.put("value", obj);
		}
		else {
			Map ms = (Map)obj;
			for (Object key :ms.keySet()) {
				context.put(key.toString(), ms.get(key));
			}
		}
	}

	private Object getRequestMap(HttpServletRequest request) {
		Map<String, String[]> nm = request.getParameterMap();
		if (nm == null || nm.size() < 1)
			return null;
		Map m = new HashMap();
		for (Map.Entry<String, String[]> entry : nm.entrySet()) {  
			m.put(entry.getKey(), entry.getValue()[0]);
		}

		return m;
	}

	private Map getSessionMap(HttpSession session) {
		Map map = new HashMap();
		Enumeration e = session.getAttributeNames();
		for (Object obj = e.nextElement(); obj != null; obj = e.nextElement()) {
			map.put(obj, session.getAttribute(obj.toString()));
		}
		return map;
	}

}
